﻿// Copyright (C) 2012 Jeff Tanner <jeff00seattle@gmail.com>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

using System;
using System.Collections.Generic;
using System.Collections;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.IO;
using System.Net;
using System.Reflection;
using System.Web;
using System.Xml;
using System.Xml.Linq;

namespace WindowsAzure_REST
{
    public class TableREST : BaseREST
    {
        // Constructor.
        
        public TableREST(string storageAccount, string storageKey) : base("http://" + storageAccount + ".table.core.windows.net/", storageAccount, storageKey)
        {
            IsTableStorage = true;
        }


        // List tables.
        // Return true on success, false if not found, throw exception on error.

        public List<string> ListTables()
        {
            return Retry<List<string>>(delegate()
            {
                HttpWebResponse response;
                List<string> tables = new List<string>();

                tables = new List<string>();

                try
                {
                    response = CreateRESTRequest("GET", "Tables").GetResponse() as HttpWebResponse;

                    if ((int)response.StatusCode == 200)
                    {
                        using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                        {
                            string result = reader.ReadToEnd();

                            XNamespace ns = "http://www.w3.org/2005/Atom";
                            XNamespace d = "http://schemas.microsoft.com/ado/2007/08/dataservices";

                            XElement x = XElement.Parse(result, LoadOptions.SetBaseUri);

                            foreach (XElement tableName in x.Descendants(d + "TableName"))
                            {
                                tables.Add(tableName.Value);
                            }
                        }
                    }

                    response.Close();

                    return tables;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 404)
                        return null;

                    throw;
                }
            });
        }


        // Create a table.
        // Return true on success, false if already exists / unable to create, throw exception on error.

        public bool CreateTable(string tableName)
        {
            return Retry<bool>(delegate()
            {
                HttpWebResponse response;

                try
                {
                    string now = DateTime.UtcNow.ToString("o");

                    string requestBody = String.Format("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" +
                                          "<entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\"" +
                                          "       xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\"" +
                                          "       xmlns=\"http://www.w3.org/2005/Atom\"> " +
                                          "  <title /> " +
                                          "  <updated>" + now + "</updated> " +
                                          "  <author>" +
                                          "    <name/> " +
                                          "  </author> " +
                                          "  <id/> " +
                                          "  <content type=\"application/xml\">" +
                                          "    <m:properties>" +
                                          "      <d:TableName>{0}</d:TableName>" +
                                          "    </m:properties>" +
                                          "  </content> " +
                                          "</entry>",
                                          tableName);

                    response = CreateRESTRequest("POST", "Tables", requestBody).GetResponse() as HttpWebResponse;
                    response.Close();

                    return true;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 409)
                        return false;

                    throw;
                }
            });
        }


        // Delete table.
        // Return true on success, false if not found, throw exception on error.

        public bool DeleteTable(string tableName)
        {
            return Retry<bool>(delegate()
            {
                HttpWebResponse response;

                try
                {
                    response = CreateRESTRequest("DELETE", "Tables('" + tableName + "')").GetResponse() as HttpWebResponse;
                    response.Close();

                    return true;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 409)
                        return false;

                    throw;
                }
            });
        }


        // Insert entity.
        // Return true on success, false if not found, throw exception on error.

        public bool InsertEntity(string tableName, string partitionKey, string rowKey, object obj)
        {
            return Retry<bool>(delegate()
            {
                HttpWebResponse response;

                try
                {
                    // Create properties list. Use reflection to retrieve properties from the object.

                    StringBuilder properties = new StringBuilder();
                    properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", "PartitionKey", partitionKey));
                    properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", "RowKey", rowKey));

                    Type t = obj.GetType();
                    PropertyInfo[] pi = t.GetProperties();
                    MethodInfo mi;
                    foreach (PropertyInfo p in pi)
                    {
                        try
                        {
                            mi = p.GetGetMethod();
                            properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", p.Name, mi.Invoke(obj, null).ToString()));
                        }
                        catch (NullReferenceException)
                        {
                        }
                    }

                    string requestBody = String.Format("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" +
                                          "<entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\"" +
                                          "       xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\"" +
                                          "       xmlns=\"http://www.w3.org/2005/Atom\"> " +
                                          "  <title /> " +
                                          "  <updated>2009-03-18T11:48:34.9840639-07:00</updated> " +
                                          "  <author>" +
                                          "    <name/> " +
                                          "  </author> " +
                                          "  <id/> " +
                                          "  <content type=\"application/xml\">" +
                                          "  <m:properties>" +
                                          "{1}" +
                                          "  </m:properties>" +
                                          "  </content> " +
                                          "</entry>",
                                          tableName,
                                          properties);

                    response = CreateRESTRequest("POST", tableName, requestBody).GetResponse() as HttpWebResponse;
                    response.Close();

                    return true;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 409)
                        return false;

                    throw;
                }
            });
        }


        // Retrieve an entity. Returns entity XML.
        // Return true on success, false if not found, throw exception on error.

        public string GetEntity(string tableName, string partitionKey, string rowKey)
        {
            return Retry<string>(delegate()
            {
                HttpWebRequest request;
                HttpWebResponse response;

                string entityXml = null;

                try
                {
                    string resource = String.Format(tableName + "(PartitionKey='{0}',RowKey='{1}')", partitionKey, rowKey);

                    SortedList<string, string> headers = new SortedList<string, string>();
                    headers.Add("If-Match", "*");

                    request = CreateRESTRequest("GET", resource, null, headers);

                    request.Accept = "application/atom+xml";

                    response = request.GetResponse() as HttpWebResponse;

                    if ((int)response.StatusCode == 200)
                    {
                        using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                        {
                            string result = reader.ReadToEnd();

                            XNamespace ns = "http://www.w3.org/2005/Atom";
                            XNamespace d = "http://schemas.microsoft.com/ado/2007/08/dataservices";

                            XElement entry = XElement.Parse(result);

                            entityXml = entry.ToString();

                        }
                    }

                    response.Close();

                    return entityXml;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 409)
                        return null;

                    throw;
                }
            });
        }


        // Query entities. Returned entity list XML matching query filter.
        // Return true on success, false if not found, throw exception on error.

        public string QueryEntities(string tableName, string filter)
        {
            return Retry<string>(delegate()
            {
                HttpWebRequest request;
                HttpWebResponse response;

                string entityXml = null;

                try
                {
                    string resource = String.Format(tableName + "()?$filter=" + Uri.EscapeDataString(filter));

                    request = CreateRESTRequest("GET", resource, null, null);
                    request.Accept = "application/atom+xml,application/xml";

                    response = request.GetResponse() as HttpWebResponse;

                    if ((int)response.StatusCode == 200)
                    {
                        using (StreamReader reader = new StreamReader(response.GetResponseStream()))
                        {
                            string result = reader.ReadToEnd();

                            XNamespace ns = "http://www.w3.org/2005/Atom";
                            XNamespace d = "http://schemas.microsoft.com/ado/2007/08/dataservices";

                            XElement entry = XElement.Parse(result);

                            entityXml = entry.ToString();
                        }
                    }

                    response.Close();

                    return entityXml;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 409)
                        return null;

                    throw;
                }
            });
        }


        // Replace Update entity. Completely replace previous entity with new entity.
        // Return true on success, false if not found, throw exception on error

        public bool ReplaceUpdateEntity(string tableName, string partitionKey, string rowKey, object obj)
        {
            return Retry<bool>(delegate()
            {
                HttpWebRequest request;
                HttpWebResponse response;

                try
                {
                    string now = DateTime.UtcNow.ToString("o", System.Globalization.CultureInfo.InvariantCulture);

                    // Create properties list. Use reflection to retrieve properties from the object.

                    StringBuilder properties = new StringBuilder();
                    properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", "PartitionKey", partitionKey));
                    properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", "RowKey", rowKey));
                    properties.Append(string.Format("<d:{0} m:type=\"Edm.DateTime\">{1}</d:{0}>\n", "Timestamp", now));

                    Type t = obj.GetType();
                    PropertyInfo[] pi = t.GetProperties();
                    MethodInfo mi;
                    foreach (PropertyInfo p in pi)
                    {
                        try
                        {
                            mi = p.GetGetMethod();
                            properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", p.Name, mi.Invoke(obj, null).ToString()));
                        }
                        catch (NullReferenceException)
                        {
                        }
                    }

                    string id = String.Format("http://{0}.table.core.windows.net/{1}(PartitionKey='{2}',RowKey='{3}')", StorageAccount, tableName, partitionKey, rowKey);

                    string requestBody = String.Format("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" +
                                          "<entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\"" +
                                          "       xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\"" +
                                          "       xmlns=\"http://www.w3.org/2005/Atom\"> " +
                                          "  <title /> " +
                                          "  <updated>{0}</updated> " +
                                          "  <author>" +
                                          "    <name/> " +
                                          "  </author> " +
                                          "  <id>{1}</id> " +
                                          "  <content type=\"application/xml\">" +
                                          "  <m:properties>" +
                                          "{2}" +
                                          "  </m:properties>" +
                                          "  </content> " +
                                          "</entry>",
                                          now,
                                          id,
                                          properties);

                    string resource = String.Format(tableName + "(PartitionKey='{0}',RowKey='{1}')", partitionKey, rowKey);

                    SortedList<string, string> headers = new SortedList<string, string>();
                    headers.Add("If-Match", "*");

                    request = CreateRESTRequest("PUT", resource, requestBody, headers);

                    request.Accept = "application/atom+xml";

                    response = request.GetResponse() as HttpWebResponse;
                    response.Close();

                    return true;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 409)
                        return false;

                    throw;
                }
            });
        }


        // Merge update an entity (preserve previous properties not overwritten).
        // Return true on success, false if not found, throw exception on error.

        public bool MergeUpdateEntity(string tableName, string partitionKey, string rowKey, object obj)
        {
            return Retry<bool>(delegate() 
            {
                HttpWebRequest request;
                HttpWebResponse response;

                try
                {
                    string now = DateTime.UtcNow.ToString("o", System.Globalization.CultureInfo.InvariantCulture);

                    // Create properties list. Use reflection to retrieve properties from the object.

                    StringBuilder properties = new StringBuilder();
                    properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", "PartitionKey", partitionKey));
                    properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", "RowKey", rowKey));
                    properties.Append(string.Format("<d:{0} m:type=\"Edm.DateTime\">{1}</d:{0}>\n", "Timestamp", now));

                    Type t = obj.GetType();
                    PropertyInfo[] pi = t.GetProperties();
                    MethodInfo mi;
                    foreach (PropertyInfo p in pi)
                    {
                        try
                        {
                            mi = p.GetGetMethod();
                            properties.Append(string.Format("<d:{0}>{1}</d:{0}>\n", p.Name, mi.Invoke(obj, null).ToString()));
                        }
                        catch (NullReferenceException)
                        {
                        }
                    }

                    string id = String.Format("http://{0}.table.core.windows.net/{1}(PartitionKey='{2}',RowKey='{3}')", StorageAccount, tableName, partitionKey, rowKey);

                    string requestBody = String.Format("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" +
                                          "<entry xmlns:d=\"http://schemas.microsoft.com/ado/2007/08/dataservices\"" +
                                          "       xmlns:m=\"http://schemas.microsoft.com/ado/2007/08/dataservices/metadata\"" +
                                          "       xmlns=\"http://www.w3.org/2005/Atom\"> " +
                                          "  <title /> " +
                                          "  <updated>{0}</updated> " +
                                          "  <author>" +
                                          "    <name/> " +
                                          "  </author> " +
                                          "  <id>{1}</id> " +
                                          "  <content type=\"application/xml\">" +
                                          "  <m:properties>" +
                                          "{2}" +
                                          "  </m:properties>" +
                                          "  </content> " +
                                          "</entry>",
                                          now,
                                          id,
                                          properties);

                    string resource = String.Format(tableName + "(PartitionKey='{0}',RowKey='{1}')", partitionKey, rowKey);

                    SortedList<string, string> headers = new SortedList<string, string>();
                    headers.Add("If-Match", "*");

                    request = CreateRESTRequest("MERGE", resource, requestBody, headers);

                    request.Accept = "application/atom+xml";

                    response = request.GetResponse() as HttpWebResponse;
                    response.Close();

                    return true;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 409)
                        return false;

                    throw;
                }
            });
        }


        // Delete entity.
        // Return true on success, false if not found, throw exception on error.

        public bool DeleteEntity(string tableName, string partitionKey, string rowKey)
        {
            return Retry<bool>(delegate()
            {
                HttpWebRequest request;
                HttpWebResponse response;

                try
                {
                    string resource = String.Format(tableName + "(PartitionKey='{0}',RowKey='{1}')", partitionKey, rowKey);

                    SortedList<string, string> headers = new SortedList<string, string>();
                    headers.Add("If-Match", "*");

                    request = CreateRESTRequest("DELETE", resource, null, headers);

                    response = request.GetResponse() as HttpWebResponse;
                    response.Close();

                    return true;
                }
                catch (WebException ex)
                {
                    if (ex.Status == WebExceptionStatus.ProtocolError &&
                        ex.Response != null &&
                        (int)(ex.Response as HttpWebResponse).StatusCode == 409)
                        return false;

                    throw;
                }
            });
        }

    }
}
